組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

3.2 クラスの裏側

クラスは最もC++らしい機能の1つですが,基本的な仕組みはそれほど難しいものではありません.なお,仮想関数については「3.4 仮想関数の裏側」で解説することにするので,そちらを参照してください.

3.2.1 クラスのメモリ配置

C++のクラスにはいろいろなメンバーが定義されます.しかし,実現方法の観点からすれば,C++のクラスはCの構造体とそれほど大きな違いはありません.

class A
{
public:
    A();
    ~A();
    int f(int arg);
    static int g(int arg);
private:
    int a;
    char* b;
    double c;
};

上記のようなクラスがあった場合,次の構造体とほぼ同じメモリ配置になります.

struct A
{
    int a;
    char* b;
    double c;
};

図3.1は,次の条件を仮定したメモリ配置です.

sizeof(int) == 4
sizeof(char*) == 4
sizeof(double) == 8

●図3.1 クラスAのメモリレイアウト

図3.1 クラスAのメモリレイアウト

ところで,次のようなクラスを考えてみましょう.

class B
{
public:
    int func();
    char a;
protected:
    int b;
private:
    double c;
};

上記の例では,データメンバーa,b,cがprotectedやprivateといったアクセス指定子で区切られています.このような場合には,a,b,cはこの順にメモリ配置されるとはかぎらなくなります.データメンバーを記述した順に,アドレスの下位から上位に配置されるようにするためには,データメンバーの間にアクセス指定子を入れてはいけません.

また,C++では,アクセス指定子が間に入らなければデータメンバーが記述した順に配置されることは保証されますが,先頭のデータメンバーのアドレスがそのクラス型オブジェクトのアドレスに一致することは保証されません.Cの場合には,次のような構造体において,&cと&c.aが同じアドレスになることが保証されていましたが,C++ではそれが期待できないのです.

struct C
{
    int a;
    int b;
};
struct C c;

なぜなら,クラスの先頭位置には,基底クラスを持つ場合にはそのメンバーが配置されるかもしれませんし,仮想関数を持つ場合には仮想関数テーブルへのポインタなどが配置されるかもしれないからです.

3.2.2 メンバー関数

クラスのメンバー関数には,非静的メンバー関数と静的メンバー関数がありますが,まずは非静的メンバー関数について解説することにします.

先ほど例に挙げたクラスAを再び例にしましょう.

class A
{
public:
    A();
    ~A();
    int f(int arg);
    int f(int arg) const;
    static int g(int arg);
private:
    int a;
    char* b;
    double c;
};

クラスAのメンバー関数fは,次の関数とほぼ同じように振る舞います.

int f(A* this, int arg);

const付きのほうは,次の関数とほぼ同じように振る舞います.

int f(const A* this, int arg);

コンストラクタやデストラクタも,コンパイラが自動的に呼び出す点を除けば,他のメンバー関数と変わりません.なお,thisが上記のように第1引数と同じように渡されるか,他の引数はスタック渡しでthisだけがレジスタ渡しになるか,あるいはもっと別のコーリングコンベンションになるかは処理系によります.

それに対して,静的メンバー関数はthisポインタを受け取りませんので,次のような非メンバー関数とほぼ同じように振る舞います.静的メンバー関数は,クラス有効範囲にあることを除いて,通常の関数(非メンバー関数)と変わらないのです.

int g(int arg);

静的であるかどうかにかかわらず,メンバー関数名は,引数情報のほかに,どのクラスに所属しているかの情報を含めた形でマングル処理されます.たとえば,GCCの場合は次のようになります.

int A::f(int arg);
 → __ZN1A1fEi
int A::f(int arg) const;
 → __ZNK1A1fEi

3.2.3 アクセス指定子の影響

クラスのアクセス指定はコンパイル時にのみ影響を与え,実行時には何の影響も与えません.当然のことですが,アクセス指定によって隠蔽したとしても,ハード的にメモリ保護などが行われるわけではありません.したがって,逆コンパイルリストをいくら眺めてみても,元のソースでのアクセス指定を知ることはできません.どうしても逆アセンブル時にアクセス指定を知りたいのであれば,元のソースと見比べるか,アクセス指定ごとに接頭辞を付けるなどしておく必要があります.